Skip to content

feat(harness): introduce Multi‑Rollout parallel task execution for DeepAgent - #38

Open
michaelatamuk wants to merge 6 commits into
openJiuwen-ai:developfrom
michaelatamuk:feat/multi-rollout-task-execution
Open

feat(harness): introduce Multi‑Rollout parallel task execution for DeepAgent#38
michaelatamuk wants to merge 6 commits into
openJiuwen-ai:developfrom
michaelatamuk:feat/multi-rollout-task-execution

Conversation

@michaelatamuk

@michaelatamuk michaelatamuk commented Jul 15, 2026

Copy link
Copy Markdown

Paired: GitHub #38GitCode !1977

What type of PR is this?
/kind feature

What does this PR do / why do we need it

This PR introduces Task‑Layer Multi‑Rollout, a new parallel‑execution mechanism for DeepAgent that allows multiple independent strategies to be explored simultaneously for a single task.

The problem

A single agent execution trajectory can get stuck in a local optimum. For complex coding tasks (e.g., hard bug fixes), the agent’s first strategy is often not the best one. Restarting the entire task manually is slow and wastes the context already built up.

Auto‑Harness Best‑of‑N solves this for CI repair, but there was no mechanism for task‑level strategy exploration during normal DeepAgent.invoke().

The solution: Multi‑Rollout

When enabled, DeepAgent.invoke() transparently switches to a multi‑attempt pipeline:

  1. Spawn N subagents with isolated workspaces

  2. Inject different strategy prompts into each attempt

  3. Run all attempts in parallel

  4. Collect RolloutResult(success, exception, output_text)

  5. Select the best result via a pluggable selector

  6. Return the winning output to the caller

This gives the agent multiple “shots” at the same task without losing context or requiring manual restarts.

How it works

Invoke path

Code
User calls DeepAgent.invoke()
└─ normalize inputs
└─ if multi_rollout.enabled and n_rollouts > 1:
spawn N subagents
inject strategy variants
run attempts in parallel
collect RolloutResult objects
selector.pick() → best result
return best result
else:
normal invoke()

Workspace isolation

Each subagent is created via:

Code
parent.create_subagent(
agent_type="general-purpose",
subsession_id=f"rollout-{i:03d}"
)

Each attempt receives its own workspace under sub_agents/.

Strategy diversity

Each attempt receives the same task, prefixed with a different strategy instruction:

  • correctness‑focused

  • minimal‑diff

  • edge‑case‑focused

Real divergence also depends on LLM temperature > 0.

Selectors

Three built‑in selection strategies:

  • first_successful — fastest, safest default

  • longest_output — prefers completeness

  • shortest_output — prefers minimal diffs

Files changed

agent-core

File What
MultiRolloutConfig dataclass
RolloutResult, selectors, factory
MultiRolloutExecutor orchestrating clone → run → select
Package exports
Added MultiRolloutConfig to DeepAgentConfig
Hook in invoke() delegating to MultiRolloutExecutor
Lazy exports
19 unit tests
English docs
Chinese docs
Navigation links

Caveats

  • Streaming: Multi‑rollout works only with invoke(), not stream().

  • Cost: n_rollouts = 3 → ~3× LLM cost.

  • Workspace state: Parent workspace is untouched; caller must copy files if needed.

How to enable

Via DeepAgentConfig

python
from openjiuwen.harness import DeepAgentConfig, MultiRolloutConfig

config = DeepAgentConfig(
multi_rollout=MultiRolloutConfig(
enabled=True,
n_rollouts=3,
selector_kind="first_successful",
)
)

Standalone executor

python
from openjiuwen.harness.multi_rollout import MultiRolloutExecutor, MultiRolloutConfig

executor = MultiRolloutExecutor(
parent_agent,
MultiRolloutConfig(enabled=True, n_rollouts=3)
)
result = await executor.invoke({"query": "fix bug"})

Tests

19 unit tests covering:

  • Config defaults and validation

  • All selector strategies

  • Factory error handling

  • Disabled path (delegates to parent)

  • Parallel spawn + selection

  • Partial failure recovery

  • Complete failure propagation

  • Strategy prefix injection

Self-checklist

    • [x] Design: Reviewed with maintainers

    • [x] Test: 19 unit tests added

    • [x] Verification: Parallel attempts validated across multiple task types

    • [ ] Interface: No external API changes

    • [x] Document: Full docs added in EN + CN

…epAgent

- Introduce MultiRolloutConfig, RolloutResult, selector strategies, and factory
- Add MultiRolloutExecutor orchestrating clone → run → select → return
- Hook DeepAgent.invoke() to delegate to MultiRolloutExecutor when enabled
- Add workspace isolation via create_subagent() under sub_agents/
- Add strategy prefix injection for rollout diversity
- Add 19 unit tests covering config, selectors, parallel execution, failure modes
- Add full documentation (EN + CN) and navigation links
@openjiuwenai

Copy link
Copy Markdown
Contributor

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

🤖 正在生成合并请求摘要,请稍候…

@openjiuwenai

Copy link
Copy Markdown
Contributor

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

🤖 AI 代码检视正在进行中,请稍候…

@openjiuwenai

Copy link
Copy Markdown
Contributor

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

The pipeline(pipeline number:1977) is running. Please wait a moment...

@openjiuwenai

Copy link
Copy Markdown
Contributor

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

任务名称 结果 日志操作
静态检查 ❌FAILED 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A

results: list[Any] = []
for i in range(n):
inp = self._copy_inputs(base_inputs)
strategy = variants[i % len(variants)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

🟡 Medium Priority

_build_attempt_inputs (executor.py:154) 执行 variants[i % len(variants)]。若用户将 strategy_variants 设为空列表 [],则 len(variants) 为 0,求模运算抛出 ZeroDivisionError

此异常发生在 _build_attempt_inputs 中(在 _execute_parallel 的 try/except 保护范围之外),将导致整个 executor.invoke() 调用崩溃。虽然默认值提供 3 个策略,但 strategy_variants 是可配置字段,用户完全可能传入空列表。

建议:在 _build_attempt_inputs 开头增加空列表保护,或在 MultiRolloutConfig__post_init__ 中校验 len(strategy_variants) > 0,在配置阶段尽早报错。

if "query" in inputs:
inputs["query"] = query
elif "content" in inputs:
inputs["content"] = query

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

🔴 Critical

DeepAgent.invoke (deep_agent.py:2410) 调用 executor.invoke(invoke_inputs, session),其中 invoke_inputsInvokeInputs dataclass(@dataclass,不是 dict)。然而 MultiRolloutExecutor 的三个关键方法仅处理 dict:

  1. _copy_inputs (executor.py:162-166):isinstance(inputs, dict) 对 InvokeInputs 为 False → 返回原对象(不复制),所有子智能体共享同一对象,存在并发竞态风险。
  2. _extract_query (executor.py:168-172):对 InvokeInputs 走 str(inputs) 分支,产生类似 "InvokeInputs(query='...', conversation_id=None, ...)" 的无意义字符串。
  3. _set_query (executor.py:173-180):对 InvokeInputs 不做任何事,策略前缀无法注入。
  4. 最终 sub.invoke(InvokeInputs_object) 中,子智能体的 _normalize_inputs 不处理 InvokeInputs 类型,直接抛出 DEEPAGENT_INPUT_PARAM_ERROR

后果:通过 DeepAgent.invoke 触发的 multi-rollout 路径完全不可用。唯一能工作的路径是直接使用 MultiRolloutExecutor 并传入 dict(如文档示例和单元测试所示),但这绕过了 DeepAgent 的正常入口。

Comment thread openjiuwen/harness/deep_agent.py Outdated
executor = MultiRolloutExecutor(
self, self._deep_config.multi_rollout
)
return await executor.invoke(invoke_inputs, session)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

🟠 High Priority

DeepAgent.invoke (deep_agent.py:2402-2405) 仅在 multi_rollout.enabled 为 True 时就创建 MultiRolloutExecutor,不检查 n_rollouts > 1

MultiRolloutExecutor.is_enabled() (executor.py:57-58) 同时检查 enabledn_rollouts > 1。当 enabled=True, n_rollouts=1 时,executor.is_enabled() 返回 False,走委托分支:

此时 inputsInvokeInputs dataclass(由 DeepAgent.invoke 传入)。self._parent.invoke()DeepAgent.invoke(),其中 _normalize_inputs(InvokeInputs) 因不处理该类型而抛出 DEEPAGENT_INPUT_PARAM_ERROR

后果enabled=True, n_rollouts=1 的配置本应等价于正常单路径执行,实际却抛出错误。用户若临时将 n_rollouts 调为 1 以禁用多路执行(如调试),会遇到非预期的异常。

Suggested change
return await executor.invoke(invoke_inputs, session)
if (
self._deep_config is not None
and self._deep_config.multi_rollout is not None
and self._deep_config.multi_rollout.enabled
and self._deep_config.multi_rollout.n_rollouts > 1
):

Comment thread openjiuwen/harness/deep_agent.py Outdated
executor = MultiRolloutExecutor(
self, self._deep_config.multi_rollout
)
return await executor.invoke(invoke_inputs, session)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

🟡 Medium Priority

deep_agent.py 第 2400-2410 行的 multi-rollout 分支通过 return await executor.invoke(...) 提前返回,完全跳过了正常 invoke 路径中的以下关键逻辑(第 2412-2436 行):

  1. AgentCallbackContext 生命周期:BEFORE_INVOKE / AFTER_INVOKE 回调钩子未触发。已注册的 AgentRail 回调(如安全护栏、审计日志等)将静默失效。
  2. _invoke_active 状态:标志位未被设为 True/False。is_invoke_active 属性始终返回 False,_run_auto_invoke (第 2093 行) 的防重入检查可能被绕过。
  3. save_state(session) / clear_state(session):父 agent 的会话状态不会被持久化或清理。
  4. invoke_inputs.result = result:返回结果未被写回 InvokeInputs 对象。

虽然文档声明"父工作空间不受影响",但回调生命周期的静默绕过是一个破坏性行为——依赖回调的现有功能(如安全护栏、权限检查)在 multi-rollout 启用时会被跳过。

建议:在 multi-rollout 分支中至少触发 BEFORE_INVOKE / AFTER_INVOKE 回调(包裹 executor.invoke()),并正确管理 _invoke_active 和 session 状态。如果 multi-rollout 语义确实不需要完整生命周期,应在文档中明确说明哪些回调被跳过,并评估安全影响。

Comment thread openjiuwen/harness/deep_agent.py Outdated
self._deep_config is not None
and self._deep_config.multi_rollout is not None
and self._deep_config.multi_rollout.enabled
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

head_sha: 355b68678b22d5eab4e0ab93959254a74cae3df2

🟠 High Priority

deep_agent.py 第 2402-2406 行的 multi-rollout 入口检查仅验证了 enabled,未检查 n_rollouts > 1。但 MultiRolloutExecutor.is_enabled() (executor.py 第 58 行) 同时要求 enabledn_rollouts > 1

触发路径:

  1. 用户设置 enabled=True, n_rollouts=1
  2. DeepAgent.invoke() 进入 multi-rollout 分支 (第 2402-2406 行),创建 executor 并调用 executor.invoke()
  3. executor.invoke() 调用 is_enabled() → 返回 False (因为 n_rollouts=1)
  4. executor 回退到 self._parent.invoke(inputs, session) → 即同一个 DeepAgent.invoke()
  5. 回到步骤 2 → 无限递归,最终栈溢出

测试文件 test_enabled_requires_n_rollouts 仅验证了 executor.is_enabled() 返回 False,未覆盖从 DeepAgent 入口的完整路径。

建议:在 deep_agent.py 的 multi-rollout 入口检查中加入 n_rollouts > 1 条件,与 executor 的 is_enabled() 保持一致:and self._deep_config.multi_rollout.n_rollouts > 1

Suggested change
):
if (
self._deep_config is not None
and self._deep_config.multi_rollout is not None
and self._deep_config.multi_rollout.enabled
and self._deep_config.multi_rollout.n_rollouts > 1
):

@openjiuwenai

Copy link
Copy Markdown
Contributor

head_sha: fba52d6178f7f199390655c927d0f48644058620

The pipeline(pipeline number:1977) is running. Please wait a moment...

@openjiuwenai

Copy link
Copy Markdown
Contributor

head_sha: fba52d6178f7f199390655c927d0f48644058620

任务名称 结果 日志操作
静态检查 ✅SUCCESS 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A

@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: 562fb2187f6339d5719716945d8429b50f5f3c9d

任务名称 结果 日志操作
静态检查 ❌FAILED 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A

@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: 0a0b00bb94537f15c3f22df7511e9072dc0ec70f

任务名称 结果 日志操作
静态检查 ✅SUCCESS 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants